package NEHE.JOGL.Port;

import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.IOException;
import javax.media.opengl.GL;
import javax.media.opengl.GLAutoDrawable;
import javax.media.opengl.GLCanvas;
import javax.media.opengl.GLEventListener;
import javax.media.opengl.GLException;
import javax.media.opengl.glu.GLU;
import javax.media.opengl.glu.GLUquadric;
import javax.swing.*;
import com.sun.opengl.util.FPSAnimator;
import com.sun.opengl.util.texture.Texture;
import com.sun.opengl.util.texture.TextureCoords;
import com.sun.opengl.util.texture.TextureIO;
import static javax.media.opengl.GL.*;
import static java.awt.event.KeyEvent.*;

/**
 * NeHe Lesson 26: Clipping and Reflection using the stencil Buffer
 */
public class NeheJOGL26Reflection extends JPanel implements GLEventListener,
      KeyListener {
   private static final int REFRESH_FPS = 60;    // Display refresh frames per second
   private GLU glu;             // For the GL Utility
   final FPSAnimator animator;  // Used to drive display() 
   GLCanvas canvas;
   
   private float rotateAngleX = 0.0f; // Rotation angle about x-axis in degree
   private float rotateAngleY = 0.0f; // Rotation angle about y-axis in degree
   private float rotateSpeedX = 0.0f; // Rotation speed about x-axis
   private float rotateSpeedY = 0.0f; // Rotation speed about y-axis
   private float z = -7.0f; // Depth into the screen (z-axis)
   private float height = 2.0f; // Height of ball from floor

   // Quadric for drawing spheres
   private GLUquadric quadric;

   // 3 textures
   private Texture[] textures = new Texture[3];
   private String[] textureFileNames = { "images/envwall.jpg", "images/ball.jpg",
         "images/envroll.jpg" };
   private String textureFileType = ".jpg";

   // Texture image flips vertically. Shall use TextureCoords class to retrieve the
   // top, bottom, left and right coordinates.
   private float[] textureTops = new float[3];
   private float[] textureBottoms = new float[3];
   private float[] textureLefts = new float[3];
   private float[] textureRights = new float[3];

   private float[] lightAmbientValue = { 0.7f, 0.7f, 0.7f, 1.0f }; // Ambient light value in RGBA
   private float[] lightDiffuseValue = { 1.0f, 1.0f, 1.0f, 1.0f }; // Diffuse light value in RGBA
   private float[] lightDiffusePosition = { 4.0f, 4.0f, 6.0f, 1.0f }; // Position of the light XYZ

   // Constructor
   public NeheJOGL26Reflection() {
      canvas = new GLCanvas();
      this.setLayout(new BorderLayout());
      this.add(canvas, BorderLayout.CENTER);
      canvas.addGLEventListener(this);
      canvas.addKeyListener(this);
      canvas.setFocusable(true);  // To receive key event
      canvas.requestFocus();
   
      // Run the animation loop using the fixed-rate Frame-per-second animator,
      // which calls back display() at this fixed-rate (FPS).
      animator = new FPSAnimator(canvas, REFRESH_FPS, true);
   }

   // Main program
   public static void main(String[] args) {
      final int WINDOW_WIDTH = 320;
      final int WINDOW_HEIGHT = 240;
      final String WINDOW_TITLE = "Nehe #26: Clipping & Reflections Using The Stencil Buffer";

      JFrame frame = new JFrame();
      final NeheJOGL26Reflection joglMain = new NeheJOGL26Reflection();
      frame.setContentPane(joglMain);
      frame.addWindowListener(new WindowAdapter() {
         @Override 
         public void windowClosing(WindowEvent e) {
            // Use a dedicate thread to run the stop() to ensure that the
            // animator stops before program exits.
            new Thread() {
               @Override 
               public void run() {
                  joglMain.animator.stop(); // stop the animator loop
                  System.exit(0);
               }
            }.start();
         }
      });
      frame.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
      frame.setTitle(WINDOW_TITLE);
      frame.setVisible(true);
      joglMain.animator.start(); // start the animation loop
   }

   // ------ Implement methods declared in GLEventListener ------

   /**
    * Called by the drawable immediately after the OpenGL context is initialized for
    * the first time. Can be used to perform one-time OpenGL initialization such as
    * setup of lights and display lists. Run only once.
    */
   @Override
   public void init(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // GL Utilities
      glu = new GLU();

      // Load all the texture images
      try {
         for (int i = 0; i < textures.length; i++) {
            textures[i] = TextureIO.newTexture(this.getClass().getResource(
                  textureFileNames[i]), false, textureFileType);
            // Use linear filter for texture if image is larger than the original texture
            gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            // Use linear filter for texture if image is smaller than the original texture
            gl.glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);

            TextureCoords textureCoords = textures[i].getImageTexCoords();
            textureTops[i] = textureCoords.top();
            textureBottoms[i] = textureCoords.bottom();
            textureLefts[i] = textureCoords.left();
            textureRights[i] = textureCoords.right();
         }
      } catch (GLException e) {
         e.printStackTrace();
      } catch (IOException e) {
         e.printStackTrace();
      }

      // Enable smooth shading, which blends colors nicely across a polygon, and
      // smoothes out lighting.
      gl.glShadeModel(GL_SMOOTH);
      // Set background color (in RGBA). Alpha of 0 for total transparency
      gl.glClearColor(0.2f, 0.5f, 1.0f, 1.0f);
      // Setup the depth buffer & enable the depth testing
      gl.glClearDepth(1.0f);
      // Clear The Stencil Buffer To 0
      gl.glClearStencil(0);
      gl.glEnable(GL_DEPTH_TEST); // enables depth testing
      gl.glDepthFunc(GL_LEQUAL); // the type of depth test to do
      // We want the best perspective correction to be done
      gl.glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST);

      // Set up lighting
      gl.glLightfv(GL_LIGHT0, GL_AMBIENT, lightAmbientValue, 0); // Set the ambient lighting for LIGHT0
      gl.glLightfv(GL_LIGHT0, GL_DIFFUSE, lightDiffuseValue, 0); // Set the diffuse lighting for LIGHT0
      gl.glLightfv(GL_LIGHT0, GL_POSITION, lightDiffusePosition, 0); // Set the position for LIGHT0
      gl.glEnable(GL_LIGHT0); // Enable LIGHT0
      gl.glEnable(GL_LIGHTING); // Enable Lighting

      // Set up the Quadric for drawing sphere
      quadric = glu.gluNewQuadric(); // Create a new quadric
      glu.gluQuadricNormals(quadric, GL_SMOOTH); // Generate smooth normals
      glu.gluQuadricTexture(quadric, true); // Enable texture
      // set up sphere mapping for both the s- and t-axes
      gl.glTexGeni(GL_S, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
      gl.glTexGeni(GL_T, GL_TEXTURE_GEN_MODE, GL_SPHERE_MAP);
   }

   /**
    * Called by the drawable to initiate OpenGL rendering.
    */
   @Override
   public void display(GLAutoDrawable drawable) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();
      // Clear the screen and the depth buffer
      gl.glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
      // Reset the view (x, y, z axes back to normal)
      gl.glLoadIdentity();

      // Clip plane equations for clipping the reflected image
      double eqr[] = { 0.0f, -1.0f, 0.0f, 0.0f };

      // Translate downward to see the floor, and z
      gl.glTranslatef(0.0f, -0.6f, z);

      // Set the color mask RGBA to false, i.e., no color gets thru
      gl.glColorMask(false, false, false, false);

      // Setting up the stencil buffer and stencil testing
      gl.glEnable(GL_STENCIL_TEST); // Enable stencil buffer for "marking" the floor
      gl.glStencilFunc(GL_ALWAYS, 1, 1); // Always passes, 1 Bit Plane, 1 As Mask
      gl.glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE); // Set the stencil buffer to 1 where we draw any polygon
      // Keep if test fails, keep if test passes but buffer test fails
      // replace if test passes
      gl.glDisable(GL_DEPTH_TEST); // Disable depth testing

      drawFloor(gl);

      gl.glEnable(GL_DEPTH_TEST); // Enable depth testing
      gl.glColorMask(true, true, true, true); // Set color mask to let RGBA thru
      gl.glStencilFunc(GL_EQUAL, 1, 1); // We draw only where the stencil is 1
      // (i.e. where the floor was drawn)
      gl.glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); // Don't change the stencil buffer

      gl.glEnable(GL_CLIP_PLANE0); // Enable clip plane for removing artifacts
      // (when the object crosses the floor)
      gl.glClipPlane(GL_CLIP_PLANE0, eqr, 0); // Equation for reflected objects
      gl.glPushMatrix(); // Push the matrix onto the stack
      gl.glScalef(1.0f, -1.0f, 1.0f); // Mirror y-axis

      gl.glLightfv(GL_LIGHT0, GL_POSITION, lightDiffusePosition, 0); // Set up LIGHT0
      gl.glTranslatef(0.0f, height, 0.0f); // Position the ball
      gl.glRotatef(rotateAngleX, 1.0f, 0.0f, 0.0f);
      gl.glRotatef(rotateAngleY, 0.0f, 1.0f, 0.0f);
      drawBall(gl); // Draw the sphere (reflection)
      gl.glPopMatrix(); // Pop the matrix off the stack
      gl.glDisable(GL_CLIP_PLANE0); // Disable clip plane for drawing the floor
      gl.glDisable(GL_STENCIL_TEST); // We don't need the stencil buffer any more

      gl.glLightfv(GL_LIGHT0, GL_POSITION, lightDiffusePosition, 0); // Set up LIGHT0 position
      gl.glEnable(GL_BLEND); // Enable blending (otherwise the reflected object won't show)
      gl.glDisable(GL_LIGHTING); // Since we use blending, we disable Lighting
      gl.glColor4f(1.0f, 1.0f, 1.0f, 0.8f); // Set color to white with 80% alpha
      gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Blending based on source alpha and 1 minus dest alpha
      drawFloor(gl); // Draw the floor

      gl.glEnable(GL_LIGHTING); // Enable lighting
      gl.glDisable(GL_BLEND); // Disable blending
      gl.glTranslatef(0.0f, height, 0.0f); // Position the ball at proper height
      gl.glRotatef(rotateAngleX, 1.0f, 0.0f, 0.0f);
      gl.glRotatef(rotateAngleY, 0.0f, 1.0f, 0.0f);
      drawBall(gl); // Draw the ball

      // Update rotational angle 
      rotateAngleX += rotateSpeedX;
      rotateAngleY += rotateSpeedY;
      gl.glFlush();
   }

   // Render the beach ball by drawing two fully overlapped spheres
   public void drawBall(GL gl) {
      // draw the first sphere with texture "ball"
      gl.glColor3f(1.0f, 1.0f, 1.0f); // Set color to white
      textures[1].enable();
      textures[1].bind();
      glu.gluSphere(quadric, 0.35f, 32, 16); // Draw first sphere
      textures[1].disable();

      // Draw the second sphere using texture "EnvRoll", set the alpha value to 40%
      // and enable blending based on the source alpha value. Enables sphere mapping.
      // The final result is a reflection that almost looks like bright points 
      // of light mapped to the beach ball.
      // Because we enable sphere mapping, the texture is always facing the viewer, 
      // even as the ball spins.
      // We blend so that the new texture doesn't cancel out the old texture
      // (a form of multi-texturing). 

      textures[2].enable();
      textures[2].bind();
      gl.glColor4f(1.0f, 1.0f, 1.0f, 0.4f); // Set color to white with 40% alpha
      gl.glEnable(GL_BLEND); // Enable blending
      gl.glBlendFunc(GL_SRC_ALPHA, GL_ONE); // Set blending mode to mix based on source alpha
      gl.glEnable(GL_TEXTURE_GEN_S); // Enable sphere mapping on s-axis
      gl.glEnable(GL_TEXTURE_GEN_T); // Enable sphere mapping on t-axis
      glu.gluSphere(quadric, 0.35f, 32, 16); // Draw another sphere using new texture
      // Textures will mix creating a multi-texture effect (Reflection)
      gl.glDisable(GL_TEXTURE_GEN_S); // Disable sphere mapping
      gl.glDisable(GL_TEXTURE_GEN_T); // Disable sphere mapping
      gl.glDisable(GL_BLEND);
      textures[2].disable();
   }

   // Render the floor
   private void drawFloor(GL gl) {
      textures[0].enable(); // "evnwall"
      textures[0].bind();
      gl.glBegin(GL_QUADS);
      gl.glNormal3f(0.0f, 1.0f, 0.0f); // Normal pointing up
      gl.glTexCoord2f(textureLefts[0], textureBottoms[0]); // Bottom-left of texture
      gl.glVertex3f(-2.0f, 0.0f, 2.0f); // Bottom-Left corner of the floor
      gl.glTexCoord2f(textureLefts[0], textureTops[0]); // Top-left of texture
      gl.glVertex3f(-2.0f, 0.0f, -2.0f); // Top-left corner of the floor
      gl.glTexCoord2f(textureRights[0], textureTops[0]); // Top-right of texture
      gl.glVertex3f(2.0f, 0.0f, -2.0f); // Top-right corner of the floor
      gl.glTexCoord2f(textureRights[0], textureBottoms[0]); // Bottom-right of texture
      gl.glVertex3f(2.0f, 0.0f, 2.0f); // Bottom-right corner of the floor
      gl.glEnd();
   }

   /**
    * Called by the drawable during the first repaint after the component has been
    * resized. Run at least once when the window is first set to visible.
    */
   @Override
   public void reshape(GLAutoDrawable drawable, int x, int y, int width, int height) {
      // Get the OpenGL graphics context
      GL gl = drawable.getGL();

      if (height == 0) {
         height = 1; // prevent divide by zero
      }
      float aspect = (float)width / height;

      // Set the viewport (display area) to cover the entire window
      gl.glViewport(0, 0, width, height);

      // Enable perspective view (where far objects are smaller)
      gl.glMatrixMode(GL_PROJECTION);
      gl.glLoadIdentity(); // reset
      // View angle of 45 degrees, based on this canvas's aspect ratio. The 0.1f,
      // 100.0f is the starting and ending points for how deep we can draw into
      // the screen.
      glu.gluPerspective(45.0f, aspect, 0.1f, 100.0f); // fovy, aspect, zNear, zFar

      // Enable the model view - any new transformations will affect the model-view
      // matrix
      gl.glMatrixMode(GL_MODELVIEW);
      gl.glLoadIdentity(); // reset
   }

   /**
    * Called when the display mode (eg. resolution) has been changed.
    */
   @Override
   public void displayChanged(GLAutoDrawable drawable, boolean modeChanged,
         boolean deviceChanged) {}

   @Override
   public void keyPressed(KeyEvent e) {
      switch (e.getKeyCode()) {
         case VK_RIGHT:
            rotateSpeedY += 0.08f;
            break;
         case VK_LEFT:
            rotateSpeedY -= 0.08f;
            break;
         case VK_DOWN:
            rotateSpeedX += 0.08f;
            break;
         case VK_UP:
            rotateSpeedX -= 0.08f;
            break;
         case VK_A:
            z += 0.05f;
            break;
         case VK_Z:
            z -= 0.05f;
            break;
         case VK_PAGE_UP:
            height += 0.03f;
            break;
         case VK_PAGE_DOWN:
            height -= 0.03f;
            break;
      }
   }

   @Override
   public void keyReleased(KeyEvent e) {}

   @Override
   public void keyTyped(KeyEvent e) {}
}
